Skip to content

Casadesign - DRAFT DO NOT MERGE#7051

Draft
giacoelho wants to merge 202 commits into
mainfrom
casadesign
Draft

Casadesign - DRAFT DO NOT MERGE#7051
giacoelho wants to merge 202 commits into
mainfrom
casadesign

Conversation

@giacoelho

Copy link
Copy Markdown
Collaborator

This is a work in progress. Will flag when it is ready to merge.

gia and others added 12 commits July 9, 2026 17:49
Introduce Tailwind v4 alongside the existing Bootstrap stack so the UI can be
modernized page-by-page without breaking current views.

- Add tailwindcss + @tailwindcss/cli (v4), build:tailwind[:dev] npm scripts,
  and a `tw` process in Procfile.dev. Output lands in app/assets/builds, which
  Sprockets already serves via the manifest's link_directory.
- Add app/assets/stylesheets/tailwind.css: CSS-first design system (Inter font
  + an indigo "brand" palette) — the visual direction for the redesign.
- New Tailwind-only layout casa_auth.html.erb (split brand panel + form),
  used by both the user and all-casa sign-in controllers.
- Redesign devise/sessions/new.html.erb in a clean, modern Stripe/Airbnb style
  and wire up remember-me properly.

Scoped to sign-in only; every other page keeps the Bootstrap `application`
layout untouched. First step of an incremental migration.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Approved font direction: swap the design-system typeface and the auth
layout's webfont link from Inter to Figtree (warmer humanist sans).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- design.md: living design-system doc (Tailwind v4 approach, Figtree type scale,
  color tokens, icon-tile pattern, component + empty-state specs, app-shell spec,
  migration checklist). Codifies the icon-tile treatment (icons on a soft colored
  background) for stat/status icons and reserves initial-avatars for people only.
- CLAUDE.md: point to design.md and record the casadesign commit/push-per-checkpoint
  workflow.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
New Tailwind-only layout: left sidebar with Pundit-guarded nav mirroring the
existing routes, plus a top bar (notifications, profile menu, sign out). Coexists
with the Bootstrap `application` layout — redesigned authenticated pages opt in via
`layout "casa_app"`. Icons via Bootstrap Icons CDN + Figtree (per design.md).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Supervisors now land on a real dashboard (DashboardController#show renders it in the
casa_app shell) instead of the Bootstrap volunteers table:

- SupervisorDashboard service builds per-volunteer follow-up status + summary stats
  from the supervisor's assigned volunteers.
- View: KPI cards, a "Needs your attention" list (icon-tile treatment per design
  feedback — replaces the ringed avatars), and a roster with color-coded
  contact-status pills.
- Empty states handled: no volunteers -> welcome + CTAs; nobody behind -> all
  caught up. The full volunteer management table remains at /volunteers.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Feedback from design review:
- Avatar initials strip honorific prefixes (Mrs./Mr./…) and use first + last name
  only, via a shared `avatar_initials` helper (removed the duplicate in the service).
- Top-bar profile menu is triggered by the avatar alone; the name lives in the
  dropdown, not the trigger.
- Page header is the page name ("Dashboard") + <title>, not a greeting.

Accessibility (WCAG AA targets):
- Skip-to-content link, landmark labels, aria-labels on icon-only controls,
  aria-current on the active nav item, table scope + sr-only caption, visible
  focus rings, contrast bumps (slate-400 -> 500/600), role=status/alert on flash.

Responsive:
- Sidebar becomes an off-canvas drawer below lg with an accessible toggle
  (button + aria-expanded, Escape/backdrop to close) and a responsive grid/padding.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Names now render as first + last only (no Mr./Mrs./...) wherever a person is shown
in the redesign — the shell sidebar/profile menu and the supervisor dashboard
(names and aria-labels), plus the avatar initials.

- New `NamePresentation.strip_honorific` + a `display_person(user)` view helper.
- Presentation-only: the stored `display_name` is left untouched. (An attempted
  `User#display_name` override was reverted — a security spec requires display_name
  to round-trip raw input unchanged, e.g. dangerous strings.)
- Documented in design.md + CLAUDE.md so it's applied on every page going forward.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The bottom-of-sidebar user block (avatar + name + role) duplicated the identity
already shown by the top-right avatar account menu, and was non-interactive. Drop
it; the top-right menu is the single source for identity + account actions.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The "CV" placeholder brand mark wasn't a value-add at 36px (the org name already
identifies the chapter) and using the real org logo there would add image/variant
infrastructure for little gain. Show the org name alone in the sidebar header; the
full logo stays for contexts with room (sign-in, court reports). design.md updated.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Bring the rest of the auth flow in line with the redesigned sign-in:
- Forgot password (passwords/new), reset password (passwords/edit), and
  accept-invitation (invitations/edit) rebuilt in Tailwind, matching the sign-in's
  inputs/buttons/error styling. CASA's email-or-phone reset is preserved.
- Users::PasswordsController uses `layout "casa_auth"`; the invitation-accept action
  renders in casa_auth (admin invite `new`/`create` untouched).
- casa_auth `<title>` is now driven by `content_for(:page_title)`; each page sets its
  own title (Sign in / Reset your password / Choose a new password / Accept your invitation).

Verified all three render 200 in the new layout (accept-invitation via a valid token).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Apply the honorific-free naming rule to the existing Bootstrap UI, not just the
redesign:
- 37 `.display_name` person-name sites across ~20 legacy views now render via the
  `formatted_name` helper (org names, form-value symbols, and the checkbox-id slug
  line were intentionally excluded).
- The 4 DataTables (volunteer, supervisor, case-contact, reimbursement) strip
  honorifics on their Ruby name outputs (display_name + supervisor_name); SQL
  select/order strings are untouched.
- Added a `formatted_name(name)` string helper alongside `display_person`, both
  backed by `NamePresentation`.

Verified /supervisors, /volunteers, /casa_cases render 200 with zero honorifics;
datatable classes load clean; standardrb + rendering green. Docs updated.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The header helper text becomes a short single sentence with no em-dash; the org
name already appears in the sidebar header, so it's no longer repeated here.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@github-actions github-actions Bot added dependencies Touches dependency files javascript Touches JavaScript code ruby Touches Ruby code erb Touches ERB templates labels Jul 9, 2026
@giacoelho
giacoelho marked this pull request as draft July 9, 2026 23:01
gia and others added 3 commits July 10, 2026 15:34
DashboardController#show now renders the casadesign supervisor dashboard
(SupervisorDashboard + casa_app layout) for supervisors, but the request
spec still asserted the old redirect to the volunteers overview and was
failing on the branch. Assert the dashboard renders and lists an assigned
volunteer instead.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…app shell

First page-by-page migration off the legacy Bootstrap UI, following design.md.
The notifications page is linked straight from the new shell (top-bar bell +
account menu), so it was the most visible remaining Bootstrap seam.

- Render NotificationsController#index in the casa_app layout.
- Restyle the index, NotificationComponent, and patch-notes partial onto the
  design system: card list, leading icon tiles, unread dot, and a cold-start
  empty state. Switch Font Awesome (fas fa-*) to Bootstrap Icons (bi-*), which
  is the icon set actually loaded on Tailwind pages.
- Move the specs off Bootstrap-class selectors onto semantic data-* hooks
  (data-notification-list-item, data-read / data-unread-dot) without weakening
  behavior coverage. All notification/patch-note ordering, counts, and empty
  states remain asserted.

Verified: component, view, request (renders the full casa_app layout), and
system specs all pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… backlog

- design.md: reframe as the permanent source of truth to refer to for all UI
  work. Add a "Design decisions (rationale)" log capturing the why behind the
  system (Tailwind-alongside-Bootstrap, layout opt-in, indigo/slate, Figtree,
  bi-* icons, icon-tile-vs-avatar, org-name-only sidebar, presentation-only
  honorific stripping, triage dashboards, empty-state taxonomy, a11y bar,
  build/output), a repeatable "Migrating a page" playbook, and a migration
  status that points at the backlog.
- design-todo.md: new prioritized, phased backlog (the "what's left") for the
  page-by-page migration off Bootstrap, plus cross-cutting infra and known
  pre-existing debt.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@github-actions github-actions Bot added the 🧪 Tests Tests label Jul 10, 2026
gia and others added 6 commits July 10, 2026 15:54
Second app-shell leaf page (linked from the account menu). Rebuilds the whole
edit-profile screen on the design system while preserving every behavior.

- Render UsersController in the casa_app layout.
- Rewrite edit.html.erb into cards: details, account, security, communication
  preferences, plus the volunteer languages table — using the documented input,
  button, and checkbox patterns.
- Replace the Bootstrap collapse accordions (Change Password / Change Email)
  with a small `disclosure` Stimulus controller (real <button> trigger, toggles
  a hidden panel, syncs aria-expanded) instead of relying on Bootstrap JS.
- Add a reusable Tailwind `shared/_form_errors` partial (the legacy
  `shared/_error_messages` stays for Bootstrap pages); it keeps the
  `@custom_error_header` hook so "... this password change ..." still renders.
- Preserve all field labels, ids, and JS-hook classes
  (toggle-email/sms-notifications, save-preference, password-new/-confirmation,
  submit-password, current_password_email, toggle-sms-notification-event) so
  require_communication_preference.js and password_confirmation.js keep working.

Verified: users request spec + the full edit system spec pass (79 examples);
erb-lint, standardrb, and StandardJS clean; JS bundle builds.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…rofile done

Add the two components introduced by the edit-profile migration (the Tailwind
form-error summary and the disclosure/collapsible-panel pattern) to design.md,
and tick edit profile in both the design.md status and design-todo.md.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…rd subtitle

- Add a role badge (current_role as a brand-tinted pill) to the casa_app account
  menu header, under name + email — the one place identity metadata lives, and a
  parity with the role line the legacy header used to show. Industry-standard
  placement for a multi-role internal tool (keeps the sidebar/top-bar chrome clean).
- Supervisor dashboard subtitle -> "Track volunteer progress at a glance."
- Document the role badge in design.md.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The person-badge icon added no signal, so remove it. Colour the pill by role
using the design system's non-semantic accent tints — Volunteer = sky (blue),
Supervisor = violet (purple), Casa Admin = amber — so roles read apart at a
glance; unknown roles fall back to slate.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…pt WCAG AA

- Drop the shouty `uppercase` transform from all design-system labels (account
  details, dashboard + languages table headers, patch-note category, auth "or"
  divider); render them sentence case. Sentence-case isolated copy ("My
  languages", "Learning hours this year").
- Bump muted label text from slate-400 (fails AA) to slate-500/600 for contrast.
- Redesign the form-error summary (shared/_form_errors): a role="alert" card with
  a leading icon and a tidy bulleted list instead of the plain block; keeps the
  heading text + #error_explanation so specs and behavior are unchanged.
- design.md: add Sentence case + Accessibility (WCAG 2.1 AA) as standing
  conventions, and fix the muted-text token. design-todo: track the app-wide
  interactive-label sentence-case pass and a slate-400 contrast audit.

Interactive button/field labels shared across pages (Update Profile, Change
Password, field labels) are spec-coupled across users/volunteers/all-casa-admin
and a shared example, so they're deferred to a holistic pass (tracked).

Verified: 109 examples across the touched pages pass; erb-lint clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Per design feedback: one sentence, no bullets, dashboard-style icon tile.
- shared/_form_errors now renders "Unable to save: <messages via to_sentence>"
  inside a rose danger icon tile (bg-rose-100 text-rose-600, bi-exclamation-
  triangle) matching the dashboard, dropping the old heading + bulleted list.
- Update edit_spec assertions from the Rails "N errors prohibited this X from
  being saved:" text to "Unable to save".
- design.md: refresh the Form errors component note.

Verified: 79 users edit + request examples pass; erb-lint clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
gia and others added 30 commits July 21, 2026 21:12
…ount/description/Delete

Per review: go back to a divider between expenses (border-t border-slate-200 + symmetric py-4, so
Delete no longer sits flush against the next divider), restore the visible field labels, and stack
the fields vertically — amount, then description (required), then a right-aligned Delete below —
instead of the inline bordered-card layout. All JS hooks (.nested-form-wrapper, .expense-*-input,
.remove-expense-button, data-*, hidden id/_destroy) preserved.

Verified: additional_expenses system spec (9) green; erb_lint clean. design.md note updated.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…t a ghost-button row

Replace the full-width danger_outline "Delete" ghost button (its own row below the fields, which
pushed content down and added whitespace) with a compact trash icon button pinned to the top-right
of each expense row (flex items-start, h-8 w-8, rose hover). Fields stay stacked with visible
labels in the left column. Specs click the .remove-expense-button hook (the button is icon-only
now). The saved-expense confirm dialog is unchanged.

Verified: expense remove / cancel / add system specs green (in isolation; the full-file run has the
known Chrome-under-load flake on an unrelated add example); erb_lint clean. design.md updated.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…it" flash link

Replace the floating "Create another" mode checkbox with a secondary "Submit & add another" button
beside the primary Submit (the Django-style "save and add another"). The primary stays first in the
DOM so Enter submits normally and click_on "Submit" (Capybara :smart) still resolves it; the
secondary carries metadata[create_another], reusing the existing finish_editing redirect and the
used_create_another analytics scope unchanged.

Since there's no per-contact show page, the create-another success flash now carries a trusted
action link -- flash[:notice_action] = {label, path}, rendered by the casa_app flash partial as a
link_to inside the notice (not raw HTML; the :json cookie serializer drops html_safe) -- pointing
at the case-contacts list so the user can find what they just created.

Verified: new_spec (plain Submit still works + new Submit & add another with the flash link),
request form_spec (53, incl. create_another) green; standardrb + erb_lint clean. design.md updated.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… WCAG axe coverage

- Notes helper text spells out the interaction: "Check each topic you discussed, then write your
  notes in the field that appears. Unchecking a topic removes it and its notes."
- Structured-address columns rebalanced across all three forms (reimbursement + both profiles):
  city 1/2 / state 1/3 / ZIP 1/6 (was state 1/6 / ZIP 1/3) -- state is wider than ZIP since a
  state name is longer than a ZIP. Miles-driven keeps its narrow max-w (short numeric entry).
- Added an axe (WCAG 2 A/AA) system spec for the whole case-contact form
  (details/notes/reimbursement/expenses, reimbursement revealed) -- passes clean.

Verified: axe spec green; erb_lint clean. design.md updated.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…dth expense rows

- Structured-address fields (reimbursement + both profiles) now have persistent VISIBLE labels
  (line 1/2 wrapped in their own groups, city/state/zip labeled) instead of relying on placeholders
  that vanish on input; line 2 keeps an "Apartment, suite, unit" hint. Redundant placeholders dropped.
- "Total miles driven" moved into a half-width sm:grid-cols-2 cell, aligning with the details
  section's 2-col grid rhythm (was an arbitrary max-width).
- Additional-expense rows: inputs are now FULL WIDTH with a small "Remove" text action on the
  amount label's line (top-right) -- no side icon narrowing the fields, no bottom ghost-button row.
  Expenses are free-form, so the notes check-to-add pattern doesn't apply.

Verified: axe (WCAG 2 A/AA) still clean; expense add/remove/cancel, volunteer-address, and
new-contact reimbursement system specs green; erb_lint clean. design.md updated.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… the mb-1.5 token

Measured the expense-row gaps in a real browser: label->input 6px, field groups 16px, row py-4 --
already identical to the canonical labeled field (miles) and the label_class token. This tidies the
amount section to express that 6px gap with mb-1.5 on the header line (the label_class token) rather
than mt-1.5 on the input, so the code uses the token instead of re-deriving it. Render-identical.

Verified: expense add/remove system specs green; erb_lint clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…and "Add another expense"

Measured the details-input -> Add-button gap at 48px. Two causes: (1) the row's space-y-4, with the
trailing hidden id/_destroy inputs, put a 16px bottom margin on the description group (space-y
margins the non-last child) on top of the row's py-4; (2) the button wrapper added another mt-4.
Space the two groups with mt-4 on the description instead of space-y-4 on the row, and drop the
button's mt-4 -- the button now sits at the row's py-4. Measured 48px -> 16px.

Verified: expense add/remove/cancel system specs green; erb_lint clean. design.md updated.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add an alert_classes(:success|:warning|:danger|:info) + alert_icon token (design_system_helper,
sibling of button_classes): the shared design-system alert card (rounded-lg border + leading
severity icon), full class strings written out for the Tailwind scanner.

- Flash banners: extract shared/_flashes, rendered by EVERY layout (casa_app, casa_auth,
  all_casa_admin) via render "shared/flashes", wrapper_class:. Replaces three drifted inline copies
  (casa_app had notice_action + alert/type classes; casa_auth differed; all_casa_admin dropped the
  type classes). notice -> success, else -> warning; keeps the alert/<type> classes, header-flash
  container, a11y role, and the notice_action link.
- Form errors: shared/_form_errors now uses the SAME alert card (danger variant) so the summary
  matches the flash banners. It's the only error summary now -- deleted the legacy bulleted
  shared/_error_messages and the bridge casa_admins/_errors and migrated their all-casa / casa-admin
  pages onto _form_errors (id="error_explanation" + alert class kept; "Unable to save:" + to_sentence).
- Specs: the four "N errors prohibited..." assertions -> "Unable to save" (the specific messages are
  still shown via to_sentence).

Verified: axe (WCAG) on casa_app/casa_auth + the form; all_casa_admin, casa_admins, password_change,
volunteers/invite (.notice + .header-flash), users/edit + supervisors (#error_explanation), and the
casa_org request -- all green; standardrb + erb_lint clean. design.md updated.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… errors (novalidate)

The form had no `novalidate`, so the browser's native HTML5 validation (required / type / min) fired
its own bubble and blocked the submit -- the design-system treatment (.field_with_errors rose
borders + the _form_errors summary, both already styled) never got to render. Add novalidate: true
so an invalid submit reaches the server and re-renders with the design-system validation. Native
bubbles can't be styled, so this is the only way to align field-level validation.

Also fix four stale specs that still checked the removed "Create another" checkbox (from the earlier
two-button change) -> click "Submit & add another". (Missed before because I re-ran only a subset;
the full new_spec run here surfaced them.)

Verified: new (29), edit + additional_expenses (17), contact_topic_answers system specs green;
erb_lint clean. design.md documents the novalidate requirement.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ign-system errors show

Extend the case-contact fix to every form. A global handler in application.js sets form.noValidate
on all forms on load / turbo:load / turbo:frame-load (Turbo Drive is off, so it runs on each full
page load), with a data-native-validation opt-out. So on any of the ~24 forms with required/type/min
fields, an invalid submit now reaches the server and renders the design-system validation
(.field_with_errors rose borders + the _form_errors summary) instead of the browser's unstyleable
native bubbles.

Verified across areas: case contacts (new/edit/expenses), casa_cases, court_dates, supervisors,
learning_hours new specs -- all green; standardjs clean. design.md updated.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…case-contact form

The banner was a run-on to_sentence with mixed casing ("Contact Type"), a doubled period
("reimbursement., and"), and only the date field got a red border -- color alone, and not every
error was surfaced at its field.

Field level: new field_error / field_error_attrs helpers render a rose message + icon under each
invalid field and tie it to the input via aria-invalid + aria-describedby (WCAG 1.4.1 -- never color
alone). Wired onto the case-contact date, contact-types, medium, duration, relevant-cases and miles
fields; the rose border now also keys off [aria-invalid]. Attribute errors show at the field;
cross-field :base errors (the reimbursement rules) stay in the summary.

Summary: shared/_form_errors now lists every error (a lone error inline, several as a list) instead
of a run-on sentence, so it matches the per-field messages with no doubled punctuation. Keeps
id=error_explanation / role=alert / the alert class / "Unable to save" (spec hooks). Shared by ~31
forms.

Message copy: validation labels are sentence case (Contact type, CASA case, Other expense
amount/details -- CASA acronym kept) and the :base sentences drop their trailing period.

Verified: case_contact model (76), case_contacts new/edit/additional_expenses/form-request,
users/edit, supervisors, error_explanation-hook specs, axe -- all green; a throwaway DOM check
confirmed aria-invalid resolves to visible messages. standardrb + erb_lint clean; design.md updated.

Known follow-up: the relevant-cases multiselect (Form::MultipleSelectComponent) shows a visible
message but not aria-invalid, since the component takes no html passthrough.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…tes)

Mirrors the existing .aider* ignore -- keeps a personal, un-shared local memory file out of the tree
(and safe from `git clean -fd`) so working notes never land in the public project CLAUDE.md.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ation

Five fixes from review of the record-new-case-contact form:
- Banner lists errors in form order, not model/validation order. shared/_form_errors takes an
  optional `order:` (attributes top-to-bottom); the case-contact form passes its field order, and
  unlisted attributes + :base fall to the end.
- Field-level message is now secondary-gray text (text-slate-500) with a centered rose icon (was
  rose text, top-aligned) -- the field border + icon carry the error color, the text stays readable.
- Field-level message renders consistently BELOW each field/group: the duration error moved out of
  the minutes grid-column to below the whole duration fieldset (marked on the fieldset, like medium).
- Miles driven is now flagged. It defaults to 0 (a valid number), so the reimbursement rule -- moved
  from :base to :miles_driven -- now marks the field + shows its message, not just the banner.
- Other expense details is now flagged: field_error added to the (casadesign-only) expense row
  partial, with a per-row unique id via form.field_id.

Verified: case_contact model (76), case_contacts new/edit/additional_expenses, users/edit, axe --
all green; a :js DOM dump confirmed form-order banner + every invalid field flagged below itself.
standardrb + erb_lint clean; design.md updated.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…rrors; softer Remove

- Expenses are now a required pair: an amount requires a description (already the rule) AND a
  description now requires an amount. So a started expense must be completed or the row left blank --
  a brand-new blank row still saves, so "Add another expense" can create it, then fill. The amount
  label gets the * and its own field_error; description was already wired.
- Field-level validation messages are sentence case (field_error upcase_first): "Can't be blank",
  not "can't be blank". (The banner was already capitalized via the attribute name.)
- The expense "Remove" action uses the documented tertiary ghost (slate) instead of rose text, which
  read as jarring.

Verified: additional_expense model (+ new amount-requires-description spec), additional_expenses
request + system, case_contacts new + form-request, expense policy -- all green; a :js dump confirmed
a description-only row flags the amount, messages are capitalized, and Remove renders slate.
standardrb + erb_lint clean; design.md updated.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…'t drop it

Blank/partial expense rows should block submit (fill both fields or Remove the row), not be silently
dropped -- a volunteer may have just forgotten to finish one. An AdditionalExpense now requires a
positive amount AND a description, enforced only once the contact is being submitted
(active_or_details?): the blank row "Add another expense" creates up front and draft autosaves are
not blocked, and the standalone create still allows the blank add. On submit, an incomplete or empty
row is flagged on both fields and blocks the form.

(reject_if: :all_blank stays: it keeps autosave from creating duplicate blank rows before the
create-on-add wires the record id; a persisted blank row still validates and flags on submit.)

Verified: additional_expense model (rewritten for the new rule + empty-row-flags-both case),
additional_expenses request + system, case_contacts new / edit / form-request -- all green; a :js run
confirmed a blank expense blocks submit with both fields flagged "Can't be blank" and listed in the
banner. standardrb clean; design.md updated.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…e-form required-field audit

The reimbursement mailing address was the one required section with no field-level treatment -- its
rule was a :base error, so it showed only in the summary, never on the fields. Move the two address
rules (blank / invalid) from :base to the :volunteer_address attribute and label it "Mailing
address", then wire field_error + aria onto the address fieldset's required parts (line 1, city,
state, zip). A blank address now flags each part (rose border + aria), shows "Must be entered for
reimbursement" under the fieldset, and lists "Mailing address must be entered for reimbursement" in
the banner (ordered right after miles).

Audited the whole form by submitting with every required field blank: relevant cases, contact date,
contact type(s), contact medium, miles driven, mailing address, and both expense fields all flag at
the field AND in the banner.

Verified: case_contact model (76), case_contacts new (29) / edit / additional_expenses / form-request
-- all green; a :js audit confirmed every required field flags. standardrb + erb_lint clean; en.yml
valid; design.md updated.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…pace-y vs m-0)

Tailwind v4 emits space-y-* as `:where(& > :not(:last-child)) { margin-block-end }` -- zero
specificity -- so the fieldsets' `.m-0` (0,1,0) overrode it and collapsed the gap to 0px after the
contact-medium and duration fieldsets (measured). Reset only the inline margin (`mx-0`) so space-y
still owns the block gap. Measured: medium -> duration now 24px (was 0), matching the other sections.
design.md documents the gotcha.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…al field_error_proc)

Roll the field-level treatment out app-wide via Rails' global field_error_proc rather than editing
each form. It keeps the .field_with_errors rose border and, for any text-like control (input /
select / textarea) whose attribute is invalid, adds aria-invalid + the same secondary-gray message +
rose icon the field_error helper produces. It SKIPS a control that already carries aria-describedby
(hand-placed via the helper -- the case-contact fieldsets), so nothing doubles; radios / checkboxes /
hidden inputs are wrapped for the border but left to their group's field_error / the summary banner.
New forms get it automatically.

Verified: proc adds the message on casa_cases/new (case_number, with aria-invalid + aria-describedby);
no doubles on the case-contact form (7 errors, 7 messages, 0 duplicate ids); Nokogiri preserves the
field HTML. No breakage across case contacts, casa_cases, court_dates, supervisors, learning_hours,
users/edit, sessions (devise), all_casa_admins, devise passwords, and the case_contact model +
form-request specs. standardrb clean; design.md documents it.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…volunteer cases)

When a case has multiple volunteers and the editor isn't one of them, the app used to disable the
reimbursement address fields (it couldn't infer whose address to save). Now the form offers a
"Reimburse which volunteer?" dropdown of the case's volunteers; picking one prefills the address
fields from that volunteer's saved address (JS) and saves the entered address back to that volunteer.

- Model: virtual reimbursement_volunteer_id; reimbursement_volunteer_options; volunteer resolves to
  the chosen one; address_field_disabled? true only when there's genuinely no volunteer; validate a
  choice is made when reimbursement is wanted.
- Controller/params: permit reimbursement_volunteer_id; save the address to the resolved volunteer.
- View: the picker (native select + chevron) above the now-enabled address fields, with a field
  error; the disabled path + reworded message now only cover the no-volunteer case.
- JS: case-contact-form#pickReimbursementVolunteer prefills the address from the selected option.

Verified: case_contact model (78, incl. new picker/validation cases), decorator (27), form-request
(53), case_contacts edit / new / additional_expenses / new_design, reimbursements request, and a new
end-to-end spec (admin picks a volunteer -> address saves to that volunteer). standardrb / standardjs
/ erb_lint clean; design.md documents it.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…sible, shared helper)

The medium icon on each case-contact card was a bare aria-hidden <i> -- unlabeled (a mystery to
sighted users) and invisible to screen readers. Add a shared contact_medium_badge(case_contact)
helper that renders the brand-tinted badge with the medium name (CaseContactDecorator#medium_label)
as both a native title tooltip and the accessible name (role=img + aria-label), with the inner <i>
aria-hidden. The card renders it via the helper now; reuse it wherever the medium icon appears.

Also fixes two stale card view-spec assertions ("Some question:" / "Additional notes:") left over
from the earlier dt/dd (colon-less) card redesign, and adds a badge spec.

Verified: card view (12), decorator (27), case_contacts index (19), axe (14) -- all green; standardrb
+ erb_lint clean; design.md documents the badge.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…nly, not Voice Only)

The medium icon's tooltip + accessible label now read in sentence case per the design system
("voice-only" -> "Voice only", "text/email" -> "Text/email"), via tr("-", " ").humanize instead of
titleize. Only the badge (contact_medium_badge) uses medium_label, so this doesn't touch the form's
medium dropdown/radios (still titleized -- a separate, broader change if we want full alignment).

Verified: decorator (33, incl. new medium_label cases), card view (12) -- green; standardrb clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…WCAG check

Full sweep so the medium reads the same everywhere. contact_mediums (the form's medium dropdown +
radios, and the index filter) and the new-design table/detail display now use
medium_type.tr("-", " ").humanize -- matching CaseContactDecorator#medium_label (the badge) -- so
"In Person" / "Voice Only" / "Text/Email" become "In person" / "Voice only" / "Text/email"
everywhere. Specs + the shared fill_in_case_contact_fields helper updated to the new labels.

WCAG (the medium badge): the icon has a text alternative via aria-label (role=img), so meaning isn't
conveyed by color or shape alone (1.1.1 / 1.4.1 / 4.1.2), and brand-600 on brand-50 is 5.6:1 (passes
1.4.11 non-text contrast). axe is clean on the index + form.

Verified: case_contacts new (29) / edit (8) / index (19) / new_design (22) / topic_answers (11),
decorator (33), card view (12), axe (14) -- all green; standardrb + erb_lint clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…JS test fixture

Cosmetic consistency with the app-wide sweep -- the fixture's radio labels: "In Person" ->
"In person", "Text/Email" -> "Text/email", "Voice Only" -> "Voice only". The test filters by element
id, not label text, so behavior is unchanged (jest 13/13).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… vs card mb-1)

The _case_contact card root carried `mb-1` (4px, specificity 0,1,0), overriding the parent stack's
zero-specificity `space-y-4` (:where(...)) on both the index and drafts -- collapsing the gap between
cards in a case from the intended 16px to 4px (pixel-measured: 16 fixed vs 4 buggy). Drop the card's
bottom margin so the stack owns the gap; `.container-fluid` stays the inert spec hook (draft-card
assertions now match `.container-fluid`, not `.container-fluid.mb-1`). Documented in design.md next to
the fieldset/m-0 instance of the same space-y trap.

Card view + index system specs green (31 examples). The medium-badge tooltip (native `title` +
aria-label) was intact in HEAD -- it had only vanished from a stale working-tree revert, now reconciled.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ve title + the hover-media gate)

The medium badge relied on a native `title` tooltip, which is easy to miss (browser delay, only on the
32px glyph, never on touch) -- reported twice. Replace it with a visible label bubble that reveals on
hover, keeping `role=img` + `aria-label` as the screen-reader name (the visible label is aria-hidden, so
AT isn't double-announced).

Crucially it is NOT built on Tailwind's `group-hover:*`: v4 gates `hover:`/`group-hover:` behind
`@media (hover: hover)`, so the tooltip would vanish on hybrid / touch-screen laptops driven by a mouse
(they report `hover: none`) -- a likely reason it "couldn't be seen". A hand-written, un-gated
`.badge-tip` rule (`.group:hover > .badge-tip`) shows on any hovering pointer.

Pixel-verified in headless Chrome (forcing :hover): opacity 0 -> 1, and the label is not clipped by the
card's overflow-hidden on tall or draft-height cards. Card view + index system specs green (31).
design.md documents the pattern + the hover-media-gate gotcha.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ne, drop the icon

Reviewer feedback: the medium icon + hover tooltip wasn't identifiable as the means of contact. The
medium IS meaningful (in person vs text matters for advocacy), but an icon is a poor signifier -- the
glyphs are ambiguous (envelope = text or letter?), the meaning hid behind a hover, and it's invisible
on touch.

Move the medium into the card's existing meta line (CaseContactDecorator#subheading) as plain text --
"July 21, 2026 | In person | 30 minutes" -- a peer to date/duration/miles, omitted when unset. This
improves BOTH cards that render subheading: the contacts index card and the case-show card.

Remove the icon from both cards, plus the now-unused contact_medium_badge helper and .badge-tip CSS.
design.md's medium section is rewritten to the meta-line pattern (with a note to never reintroduce an
icon-alone or hover-gated reveal).

decorator + card view specs (45) and index + casa_cases show system specs (58) green; erb_lint +
standardrb clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…kbox column

The bulk-assignment table's header checkbox was a bare, unlabeled box (aria-label only), so "select
all" wasn't discoverable, and its <th> lacked align-top -- it floated centered in the tall (wrapped)
header while the text headers top-aligned, reading as misaligned.

- Wrap the select-all in a visible "Select all" <label> (discoverable + labeled; the wrapping label is
  the accessible name, so the redundant aria-label is dropped).
- Add align-top to the header checkbox <th> and the per-row checkbox <td> so they line up with the
  header text / row name (the checkbox now sits within 2px of the Name header, was floating centered).

Per-row checkboxes keep their "Select {name}" aria-label (the row name is the visible context).
Verified: select-all still checks all rows and reveals "Manage volunteers (N)"; volunteers index
system spec 23/0; erb_lint clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…box + align to the text line

Follow-up to reviewer feedback: the visible "Select all" text widened the narrow checkbox column and
its color didn't match the other headers. Industry standard for a bulk table (Gmail, GitHub, Linear,
MUI/AntD) is a bare header checkbox with an accessible name, not visible column-header text -- so
revert to a bare checkbox with aria-label + title "Select all". Column back to 40px, no color mismatch.

Also fix vertical alignment (all measured to 0px offset in headless Chrome):
- mt-0.5 drops the header + row checkboxes onto the text line (checkbox center == the name's first-line
  center on every row; header checkbox center == the Name header center).
- align-top on the actions cell so Edit/Impersonate top-align with the row's first line instead of
  floating centered in tall (wrapped) rows.

Volunteers index system spec 23/0; erb_lint clean; select-all + "Manage volunteers (N)" still work.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…-wide (+ document the pattern)

The cases-index Edit button floated vertically centered while the data cells top-aligned -- a hardcoded
action <td> missing align-top. Same drift existed across supervisors / casa_admins / other_duties /
org-settings (10 casa_org tables) / hearing-types, and the dashboard tables were middle-aligned
throughout. App-wide audit + fix:

- align-top on every trailing action <td> so Edit/Delete/View top-align with the row's first line
  instead of floating centered when a neighbouring cell wraps to two lines.
- dashboard (volunteer + supervisor) tables had no align-top at all (tall status cells) -- align-top
  on all their body cells.

Checkboxes already follow the pattern: a bulk-select alone in a cell uses align-top + mt-0.5 to sit on
the adjacent column's first text line (volunteers); a checkbox with its label in the same cell uses
inline-flex items-center (self-aligning). Documented the full row-alignment pattern in design.md's
Tables section (body/action cells, checkbox-alone vs label-in-cell, bare select-all header).

Verified: cases Edit button top-aligns to the case-number line (headless, topDiff -1); casa_cases /
casa_admins / supervisors index specs 17/0; erb_lint clean on all 17 views.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…e so it top-aligns

Missed in the table-alignment audit: the "Mark complete" toggle wrapped to two lines in the narrow
reimbursement-queue column, and its `inline-flex items-center` label then centered the checkbox across
both wrapped lines -- pushing the whole control ~10px below the row's first line (measured). A
checkbox-with-label cell is NOT automatically aligned if the label wraps.

whitespace-nowrap keeps "Mark complete" on one line: the toggle now top-aligns with the row (measured
diff 0, was +10), the label is one line, and there is no horizontal overflow (scrollW 1265 < 1280).
design.md's table row-alignment pattern updated with the wrap caveat.

Reimbursement system spec 3/0; erb_lint clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

db Touches database schema or migrations dependencies Touches dependency files erb Touches ERB templates javascript Touches JavaScript code pundit Touches Pundit authorization policies ruby Touches Ruby code 🧪 Tests Tests

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants